27. 中国零售客户 RFM 探索(跨国样例对照)

本章概要

  • 本章安排:教学平台使用已经去标识的客户交易数据完成 RFM 固定练习;公开教材再用课程数据下载入口提供的中国市场数据练习分层和覆盖比较。
  • 学习材料:授权中国零售客户的客户号、日期、金额与订单号字段。
  • 本章任务:先执行RFM字段/日期/唯一键合同,再计算 Recency、Frequency、Monetary 分层。
  • 完成后你将得到:数据准备条件状态、RFM表、分层人数和可行动客户清单。
  • 自我检查:核对客户总数是否等于各层人数之和,并抽取一位客户手算 RFM;同时确认数据已经去标识且字段齐全。
  • 拓展练习:把RFM口径拓展应用到另一中国零售渠道。

本章概述

本章将学习RFM客户分层模型,通过跨国电商零售数据进行客户价值分析:

  • 理解RFM模型的三个核心维度
  • 掌握基于RFM值的客户分类方法
  • 使用柱状图和饼图进行客户结构可视化
  • 根据分层结果制定差异化营销策略

中国拓展应用任务:RFM 必须先通过数据要求

层级 可用本地依据
主体筛选 stock/stock_basic_data.h5(key: stock_basic_info)筛选沪苏浙皖零售与电商企业
财务边界 stock/financial_statement.h5(key: financial_data)可用于核对企业、期间与经营连续性
RFM 必需字段 脱敏交易表的 customer_idorder_dateorder_idamount

中国拓展应用任务:RFM 决策责任

CRM 负责人按分层规模、复购率与毛利再次检查营销动作;未通过字段判断条件时不得发布客户分层结论。

中国拓展应用任务:数据边界

仓库没有客户级交易数据,因此不伪造中国 RFM 结果。跨国样例只演示方法;本地拓展应用须先完成字段口径、脱敏授权与观察窗检查。

RFM模型:客户价值分析的经典方法

RFM模型通过三个维度衡量客户价值:

维度 英文 含义
R Recency 最近一次消费距今的时间
F Frequency 一定时期内的消费次数
M Monetary 一定时期内的消费总金额

每个维度按高/低划分,组合形成 \(2^3 = 8\) 种客户类型。

RFM八类组合先归并为四类动作

RFM四类动作优先级 将八种RFM高低组合归并为核心维护、优先唤回、提频升单和低成本观察四类动作 先识别组合,再归并动作 核心维护 代表组合:R高 · F高 · M高 动作:权益维护,核对留存与毛利 优先唤回 代表组合:R低,且 F高或M高 动作:定向召回,验证回流增量 提频 / 升单 代表组合:R高,F或M仍低 动作:交叉销售,小规模试验 低成本观察 代表组合:其余低优先级组合 动作:自动触达,设置退出条件

动作只用于排序,须由毛利、渠道成本与试验结果共同核对。

数据准备:导入库与加载数据

本案例的核心步骤:

  1. 导入 pandasnumpymatplotlib 等常用库
  2. 加载包含RFM标签的Excel数据文件
  3. 对RFM列做字符串清洗(去除空格)
  4. 定义客户分类函数并应用
  5. 可视化各等级客户分布

运行前预测|平台任务:RFM客户分层完整代码

  • 输入预测:运行前先写出 rfmbarsheightha 的业务含义、数据类型或取值范围,并判断哪一个输入最可能改变结果。
  • 结果预测:不展开答案,先预测将得到values 的结果;同时写出方向、数量级或表格/图形结构。
  • 完成要求:能独立说明本任务从输入到“平台任务:RFM客户分层完整代码”结果的关键步骤,原样录入平台代码并得到可核对的运行结果。

⭐ 平台任务:RFM客户分层完整代码

展开完整代码(投影默认折叠)
# 注:skrfm.xlsx 数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
import numpy as np  # 导入NumPy数值计算库
import matplotlib.pyplot as plt  # 导入Matplotlib绘图库
import warnings  # 导入warnings模块用于控制警告输出
warnings.filterwarnings('ignore') # 忽略报错(警告)
#加载数据
rfm = pd.read_excel('skrfm.xlsx') 
rfm['RFM'] = rfm['RFM'].str.strip() # 去除空格
def trans(x):  # 定义函数trans
  if x == '高高高':  # 条件判断:x == '高高高'
    return '重要客户'  # 返回计算结果
  elif x == '高低高':  # 否则判断:x == '高低高'
    return '低购买频率客户'  # 返回计算结果
  elif x == '低高高':  # 否则判断:x == '低高高'
    return '长期未消费客户'  # 返回计算结果
  elif x == '低低高':  # 否则判断:x == '低低高'
    return '长期未消费低购买频率客户'  # 返回计算结果
  elif x == '高高低':  # 否则判断:x == '高高低'
    return '低消费客户'  # 返回计算结果
  elif x == '高低低':  # 否则判断:x == '高低低'
    return '低购买频率低消费客户'  # 返回计算结果
  elif x == '低高低':  # 否则判断:x == '低高低'
    return '长期未消费低消费客户'  # 返回计算结果
  else:  # 不满足以上条件时
    return '其他待发展客户'  # 返回计算结果
rfm['用户等级'] = rfm['RFM'].apply(trans)  # 对数据应用自定义函数
print(rfm)  # 输出变量rfm的值

rfm['用户等级'].value_counts()  # 统计用户等级列各取值的频率分布
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体或其他支持中文的字体
plt.rcParams['axes.unicode_minus'] = False  # 解决负号'-'显示为方块的问题
# 创建柱状图
plt.figure(figsize=(12, 6))
# 绘制柱状图
bars = plt.bar(rfm['用户等级'].value_counts().index, rfm['用户等级'].value_counts().values, color='blue',alpha=0.4) 
# 添加标题和标签
plt.title('各等级类型用户数', fontsize=16)
plt.xlabel('用户等级类型', fontsize=12)  # 设置X轴标签
plt.ylabel('用户数', fontsize=12)  # 设置Y轴标签
plt.xticks(rotation=45, ha='right') # 旋转x轴标签以便更好地显示

# 在柱子上方显示数值
for bar in bars:
  height = bar.get_height()  # 获取当前柱子的高度值(用于数据标注)
  plt.text(bar.get_x() + bar.get_width() / 2., height,  # 添加文本标注
       f'{height:,}',  # 格式化标注文本(千分位分隔)
       ha='center', va='bottom', fontsize=9)  # 设置标注文本的水平和垂直对齐方式

# 调整布局防止标签被截断
plt.tight_layout()
# 显示图形
plt.savefig("7.png")

#绘制饼图
user_levels = rfm['用户等级'].value_counts()
labels = user_levels.index # 用户等级的类别
values = user_levels.values # 每个类别的数量

# 创建饼图
plt.figure(figsize=(10, 6)) # 设置画布大小
# 绘制饼图
plt.pie(values, labels=labels, autopct='%1.1f%%', startangle=90, textprops={'fontsize': 12, 'color': 'white'})
# 添加标题和图例
plt.title('用户等级类型占比', fontsize=16)
plt.legend(labels, title="用户等级", loc="best",bbox_to_anchor=(1, 0.5)) # 图例位置自动调整为最佳位置
# 显示图表
plt.savefig("8.png")

任务复盘|平台任务:RFM客户分层完整代码

运行后核对:核对 rfmbarsheightha 是否按预测参与运算,实际输出是否与预测一致;若不一致,先检查类型、单位、索引/字段和运算顺序。

拓展练习:只改变一个关键输入或业务场景,先预测输出如何变化,再运行验证并解释变化原因。

代码解读:导入库与数据加载

import pandas as pd       # 数据分析核心库
import numpy as np        # 数值计算库
import matplotlib.pyplot as plt  # 绘图库
import warnings
warnings.filterwarnings('ignore')  # 忽略警告信息

数据加载与清洗

rfm = pd.read_excel('skrfm.xlsx')   # 加载Excel数据
rfm['RFM'] = rfm['RFM'].str.strip() # 去除RFM列字符串两端空格

代码解读:客户分类函数 trans()

核心思路:根据RFM组合值(如’高高高’)映射为客户等级名称。

  • if x == '高高高' → 返回 '重要客户'
  • elif x == '高低高' → 返回 '低购买频率客户'
  • elif x == '低高高' → 返回 '长期未消费客户'
  • …以此类推,共8种分支

应用函数

rfm['用户等级'] = rfm['RFM'].apply(trans)

apply() 对每一行RFM值调用 trans 函数,自动生成新列。

代码解读:更优雅的字典映射写法

展开完整代码(投影默认折叠)

# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行
# ==================== 定义客户分类函数 ====================
def classify_customer(rfm_value):
    """根据RFM值分类客户,返回客户等级"""
    mapping = {
        '高高高': '重要客户',  # R、F、M三个维度都高,是最有价值的客户
        '高低高': '低购买频率客户',  # 最近消费、金额高,但频率低,有提升空间
        '低高高': '长期未消费客户',  # 频率高、金额高,但很久没消费,需要召回
        '低低高': '长期未消费低购买频率客户',  # 金额高但频率低且很久没消费
        '高高低': '低消费客户',  # 最近消费、频率高但金额低,适合培养
        '高低低': '低购买频率低消费客户',  # 最近消费但频率和金额都低
        '低高低': '长期未消费低消费客户'  # 很久没消费且金额低,需要激活或放弃
    }
    return mapping.get(rfm_value, '其他待发展客户')  # 如果RFM值不在映射表中,归为"其他待发展客户"

# ==================== 应用分类函数 ====================
rfm['用户等级'] = rfm['RFM'].apply(classify_customer)
# 对每一行RFM值应用classify_customer函数,生成用户等级标签
# apply()是Pandas中高效处理行/列数据的方法

print('用户等级分布:')  # 输出标题
print(rfm['用户等级'].value_counts())  # 统计每个等级的用户数量,了解客户结构

字典映射 vs if-elif:两种写法对比

对比维度 if-elif 写法 字典映射写法
代码量 多个 if/elif 分支 一个字典 + get()
可读性 较冗长 简洁直观
可维护性 新增类型需加分支 新增类型只需加键值对
执行效率 逐个判断,O(n) 单次哈希查找平均 O(1)
推荐场景 复杂条件判断 简单值映射

结论:对于RFM这类「值→标签」的简单映射,字典写法更优

可视化:柱状图——各等级用户数分布

展开完整绘图代码

# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行
# ==================== 设置中文字体支持 ====================
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC']  # 使用课程规定且已安装的思源宋体显示中文
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

# ==================== 统计各等级用户数量 ====================
user_counts = rfm['用户等级'].value_counts()  # 统计每个用户等级的出现次数

# ==================== 创建柱状图 ====================
plt.figure(figsize=(12, 6))  # 创建12x6英寸的画布
bars = plt.bar(
    user_counts.index,  # x轴为用户等级
    user_counts.values,  # y轴为用户数量
    color='steelblue',  # 设置柱状图颜色为钢蓝色
    alpha=0.6  # 设置透明度为0.6,使图表更加柔和
)

# ==================== 添加数值标签 ====================
for bar in bars:  # 遍历每个柱子
    height = bar.get_height()  # 获取柱子的高度(即用户数量)
    plt.text(
        bar.get_x() + bar.get_width() / 2.,  # x坐标为柱子中心
        height,  # y坐标为柱子顶部
        f'{int(height):,}',  # 显示数值,使用千位分隔符
        ha='center',  # 水平居中对齐
        va='bottom',  # 垂直底部对齐
        fontsize=10  # 字体大小为10
    )

plt.title('各等级类型用户数', fontsize=16)  # 设置图表标题
plt.xlabel('用户等级类型', fontsize=12)  # 设置x轴标签
plt.ylabel('用户数', fontsize=12)  # 设置y轴标签
plt.xticks(rotation=45, ha='right')  # x轴标签旋转45度,右对齐,避免重叠
plt.grid(axis='y', alpha=0.3)  # 显示y轴网格线,透明度0.3
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图表

柱状图代码要点解析

关键技巧

  • value_counts() 统计各等级出现频次,自动降序排列
  • plt.bar()alpha=0.6 控制透明度,使视觉更柔和
  • 数值标注循环:遍历每个柱子,在顶部添加精确数值
  • plt.xticks(rotation=45) 旋转标签防止重叠
  • plt.tight_layout() 自动调整边距防止截断

可视化:饼图——用户等级占比分布

展开完整代码(投影默认折叠)

# 注:该代码块依赖的数据来自上方平台任务代码块,因其未执行,本块也无法执行
# ==================== 准备饼图数据 ====================
user_levels = rfm['用户等级'].value_counts()  # 统计各等级用户数
labels = user_levels.index  # 饼图的标签(用户等级)
values = user_levels.values  # 饼图的数值(用户数量)

# ==================== 创建饼图 ====================
plt.figure(figsize=(10, 6))  # 创建10x6英寸的画布
plt.pie(
    values,  # 饼图的数值
    labels=labels,  # 饼图的标签
    autopct='%1.1f%%',  # 显示百分比,保留1位小数
    startangle=90,  # 从90度(正上方)开始绘制
    textprops={'fontsize': 11}  # 设置文本字体大小为11
)

plt.title('用户等级类型占比', fontsize=16)  # 设置图表标题
plt.legend(
    labels,  # 图例标签
    title='用户等级',  # 图例标题
    loc='best',  # 自动选择最佳位置
    bbox_to_anchor=(1, 0.5)  # 将图例放置在图表右侧
)
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图表

饼图代码要点解析

关键技巧

  • autopct='%1.1f%%' 自动计算并显示百分比(保留1位小数)
  • startangle=90 让第一个扇区从正上方开始,视觉更直观
  • bbox_to_anchor=(1, 0.5) 将图例移至图表右侧,避免遮挡
  • 饼图适合展示占比关系,但类别过多时建议改用柱状图

平台标签只形成待验证的试验优先级

根据RFM模型,客户群体呈现明显的分层特征

客户类型 平台标签含义 可支持的下一步
重要客户 R高/F高/M高 候选服务试验优先级
低频客户 R高/F低/M高 候选复购试验优先级
沉睡客户 R低/F高/M高 候选召回试验优先级
低价值客户 多维度为低 先核验成本与公平性,不直接降级服务

分析结论:差异化营销策略

差异化营销策略 四类客户的差异化营销策略示意图 ⭐ 重要客户 VIP专属服务 · 积分回馈 个性化推荐 · 优先售后 目标:提升忠诚度与客单价 🔄 低频客户 限时折扣 · 满减活动 定期推送 · 复购激励 目标:提高购买频率 💤 沉睡客户 召回短信 · 专属优惠券 新品推荐 · 情感关怀 目标:重新激活消费 📉 低价值客户 降低服务成本 自动化营销 · 基础维护 目标:控制运营成本

营销试验的判断条件

  • 数据说明:公开教材不使用可识别个人的客户交易表;平台给出的分类标签也不能证明营销措施有效。
  • 怎样使用结果:CRM 负责人可先安排小规模随机对照试验,不应直接批量触达客户或降低服务等级。
  • 基线与指标:比较增量毛利、复购、触达成本、退订与客诉;事先设定样本量与阈值。
  • 什么时候需要重新检查:授权或字段不完整、退订或客诉超出事先设定的范围、增量毛利不达标时,应暂停试验;在约定的试验结束日再次查看结果。

本章小结

核心知识点回顾

  • RFM模型:通过 R(最近性)、F(频率)、M(金额)三维度对客户进行价值分层
  • 字典映射:比 if-elif 更简洁高效的分类方式
  • apply() 函数:Pandas中对列/行批量应用自定义函数的利器
  • 柱状图:展示各类别的绝对数量差异
  • 饼图:展示各类别的占比关系

随堂练习

  • 问题 1|需要准备哪些数据?:教学平台练习需要客户号、日期、金额和订单号;公开教材拓展案例需要上市公司基本信息和股票行情。请分别写出字段及其含义。
  • 问题 2|需要完成哪些操作?:路线 A 仅映射平台已有 RFM 标签;路线 B 仅在授权中国交易表可得时重算 R/F/M。
  • 问题 3|应得到哪些结果?:平台练习应得到 RFM 指标和客户分层表;公开教材案例应得到中国上市公司的覆盖分层。两类结果不能互相替代。
  • 问题 4|怎样确认结果可靠?:核对客户数与各层人数之和,并抽取一位客户手算 RFM;公开教材案例则核对公司数与各层数量之和。
  • 问题 5|换一个情境,怎样继续应用?:把RFM口径拓展应用到另一中国零售渠道。
  • 作答提示:请依次写清所用数据、分析过程、所得结果、核对方法和拓展思考。课程所需数据见前言中的下载入口;教学平台固定题按页面说明完成。

教师参考解答|答案与说明 1

  • 所用数据与字段:平台内置的 skrfm.xlsx 已包含 RFM 指标,本题只根据 RFM 指标划分用户等级,不推测文件中没有提供的原始交易信息。
  • 解答示例|字典映射与平台同口径分层函数(平台保护块之外):

教师参考解答|代码 1

展开代码(代码区可独立滚动)
import pandas as pd  # 导入表格库
def map_platform_rfm(rfm_table):  # 定义平台已派生RFM表的映射接口
    if 'RFM' not in rfm_table.columns: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 缺列时停止
    mapped=rfm_table.copy()  # 保留平台表原始字段
    mapped['RFM']=mapped['RFM'].astype('string').str.strip()  # 对齐平台清洗步骤
    if mapped.empty or mapped['RFM'].isna().any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 无有效标签时停止
    level_map={'高高高':'重要客户','高低高':'低购买频率客户','低高高':'长期未消费客户','低低高':'长期未消费低购买频率客户','高高低':'低消费客户','高低低':'低购买频率低消费客户','低高低':'长期未消费低消费客户'}  # 用字典表达同一组 if-elif 分类规则
    unknown_labels=mapped.loc[~mapped['RFM'].isin(level_map),'RFM'].value_counts(dropna=False)  # 单列报告未覆盖标签
    mapped['用户等级']=mapped['RFM'].map(level_map).fillna('其他待发展客户')  # 用字典得到平台同口径结果
    counts=mapped['用户等级'].value_counts()  # 生成柱图与饼图共同输入
    if counts.sum()!=len(mapped): raise AssertionError('RFM count reconciliation failed')  # 核对分类行数守恒
    return mapped,counts,unknown_labels  # 返回映射明细、计数与未知标签检查

教师参考解答|答案与说明 2

  • 中国本地解答|规定行情资产的三维覆盖分层:该拓展应用只验证“指标构造—分位分层—动作核对”方法,不把证券分层称为客户 RFM。

教师参考解答|代码 2

展开代码(代码区可独立滚动)
from pathlib import Path  # 导入路径工具绑定规定资产
import pandas as pd  # 导入表格库以读取并标准化HDF字段
import numpy as np  # 导入近似数值核对工具
root=Path('/home/ubuntu/r2_data_mount/data/stock')  # 指定股票数据目录
basic_path,price_path=root/'stock_basic_data.h5',root/'stock_price_post_adjusted.parquet'  # 绑定基础信息与可按证券过滤的复权行情快照
if not basic_path.exists() or not price_path.exists(): raise FileNotFoundError('未找到课程股票数据文件,请从课程数据下载入口获取')  # 提示读者下载文件
basic=pd.read_hdf(basic_path,key='stock_basic_info',columns=['order_book_id','symbol','province']).rename(columns={'order_book_id':'raw_code','symbol':'name','province':'area'})  # 只读取公司筛选字段并标准化
basic['ts_code']=basic['raw_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一证券代码后缀
basic['area']=basic['area'].astype('string').str.replace(r'[省市]$','',regex=True)  # 统一省市简称
required_basic={'ts_code','name','area'}  # 定义基础信息字段结构
required_price={'ts_code','trade_date','amount'}  # 定义行情覆盖字段结构
if not required_basic<=set(basic.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 标准化后仍缺列时停止
focus=basic.loc[basic['area'].isin(['上海','江苏','浙江','安徽'])].sort_values('ts_code').head(40)  # 按固定规则选取长三角示例公司
if focus.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空对象时停止
focus_raw_codes=focus['raw_code'].tolist()  # 保留HDF原始代码以高效筛选行情
prices=pd.read_parquet(price_path,columns=['order_book_id','date','total_turnover'],filters=[('order_book_id','in',focus_raw_codes)]).rename(columns={'order_book_id':'ts_code','date':'trade_date','total_turnover':'amount'})  # 在读取层按对象过滤并标准化成交额
prices['ts_code']=prices['ts_code'].str.replace('.XSHG','.SH',regex=False).str.replace('.XSHE','.SZ',regex=False)  # 统一行情证券代码后缀
prices=prices.loc[pd.to_datetime(prices['trade_date']).between('2023-01-01','2025-12-31')].copy()  # 固定三年示例观察窗
if not required_price<=set(prices.columns): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 标准化后仍缺列时停止
market=prices.merge(focus,on='ts_code',how='inner',validate='many_to_one').copy()  # 连接行情与公司标签

教师参考解答|代码 3

展开代码(代码区可独立滚动)
market['trade_date']=pd.to_datetime(market['trade_date'])  # 统一交易日期
if focus.empty or market.empty: raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 空对象或空连接时停止
if market.duplicated(['ts_code','trade_date']).any(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 重复行情键时停止
window_end=market['trade_date'].max()  # 固定观察窗末日
coverage=market.groupby(['ts_code','name','area']).agg(recency=('trade_date',lambda dates:(window_end-dates.max()).days),frequency=('trade_date','nunique'),monetary=('amount','sum')).reset_index()  # 构造证券覆盖三指标
if len(coverage)<4 or not np.isfinite(coverage[['recency','frequency','monetary']].to_numpy()).all(): raise ValueError('输入文件、字段、样本量或数值不符合当前分析要求,请按本页说明检查')  # 四档分层与有限值检查要求
coverage['coverage_tier']=pd.qcut(coverage['frequency'].rank(method='first'),q=4,labels=['低','中低','中高','高'])  # 按活跃交易日分位分层
assert np.isclose(coverage['monetary'].sum(),market['amount'].sum(),rtol=1e-10,atol=0.01)  # 以货币容差核对成交额守恒
print(coverage.groupby(['area','coverage_tier'],observed=True).agg(securities=('ts_code','nunique'),turnover=('monetary','sum')))  # 输出地区分层规模与分母

教师参考解答|答案与说明 3

  • 解释答案:平台表已经给出 RFM 组合标签,本题只比较字典映射与 if-elif 的分类结果;分类只用于制定试验优先级,不能证明营销动作有效。中国行情拓展应用是另一个方法验证,不是客户价值替代依据。
  • 拓展应用答案:恢复 skrfm.xlsx 后核对 7 个已知标签的映射覆盖、列出落入“其他待发展客户”的未知标签,并验证用户等级计数总和等于输入行数;不在缺少原始交易字段时声称重算 R/F/M 阈值、金额或毛利。
  • 参考结果:平台练习输出 RFM、用户等级、分类计数和未知标签;公开教材案例输出长三角上市公司覆盖分层。所需中国市场数据可从课程数据下载入口取得。
  • 边界 / 局限:平台只验证既有标签映射;证券分位阈值随观察窗变化,分层不是因果效果
  • 常见错误:声称臆造的 case 路径是规定资产;从 RFM 标签虚构原始金额;把证券覆盖分层当客户RFM